Skip to content

fix(cli): #15 parts 1+2 — agentkeys run for master sessions + --env override#19

Merged
hanwencheng merged 3 commits intomainfrom
fix/issue-15
Apr 14, 2026
Merged

fix(cli): #15 parts 1+2 — agentkeys run for master sessions + --env override#19
hanwencheng merged 3 commits intomainfrom
fix/issue-15

Conversation

@hanwencheng
Copy link
Copy Markdown
Member

@hanwencheng hanwencheng commented Apr 14, 2026

Summary

Fixes parts 1 and 2 of issue #15. Part 3 (scope-edit CLI command) is tracked as a follow-up story (fix-15b in .omc/prd.json) and will ship in a separate PR.

  • Part 1 (master session support): agentkeys run now works when the active session is a master (scope: None). Previously nothing was injected — the CLI only read session.scope.services, which is empty for master sessions.
  • Part 2 (--env flag): agentkeys run --env KEY=service (repeatable) overrides the auto-convention (SERVICE_API_KEY) for services whose canonical env var doesn't match, e.g. GITHUB_TOKEN.

What changed

Trait + backend (agentkeys-core/src/backend.rs, agentkeys-mock-server/src/handlers/credential.rs, lib.rs, test_client.rs)

  • Added CredentialBackend::list_credentials(session, agent_id) -> Vec<ServiceName>.
  • HTTP endpoint: GET /credential/list?agent_id=<w> — ownership-checked, returns {"services": [...]} sorted.
  • InProcessBackend + MockHttpClient implementations added; DummyBackend stubs unimplemented!().

CLI (agentkeys-cli/src/lib.rs, main.rs)

  • cmd_run(ctx, agent, env_overrides: &[String], cmd) — signature extended.
  • When session.scope.is_none(), the CLI calls list_credentials and injects every stored credential.
  • When session.scope = Some(scope), uses scope.services unchanged.
  • --env KEY=service parsed; invalid format (no =) returns a clean error before the child is spawned.

Docs

  • docs/manual-test-issue-15.md (new) — 4 test cases including --env override and ownership enforcement.
  • docs/manual-test-stage4.md Test 9 — run SKIPPED caveat removed.
  • wiki/credential-usage.md — env-var naming convention section added.
  • docs/contradictions.md §4.2 — PARTIALLY RESOLVED (part 3 tracked for follow-up).

Test plan

  • cargo test -p agentkeys-core → 6 passed
  • cargo test -p agentkeys-cli → 18 passed (includes 4 new tests for parts 1+2)
  • cargo test -p agentkeys-mock-server → 40 passed (3 new tests for list_credentials)
  • 7 new tests covering: list_credentials returns sorted stored services, empty for unknown agent, ownership-enforced (403); cmd_run master-session injects all, child-scope respects scope list, --env override wins over auto-convention, --env invalid format returns clean error
  • Claude code-reviewer: approved after 3 blocking fixes landed in this branch:
    • list_credentials handler now filters by session.scope_json — scoped sessions can no longer enumerate service names outside their scope.
    • cmd_run pre-flight-validates every --env KEY=SERVICE entry before any credential I/O.
    • unused Serialize import removed from handlers/credential.rs.
    • Test 16 was rewritten by the executor to store credentials under the child wallet (which the master owns) — passing properly instead of #[ignore]d. Spec was updated to reflect the better design.
  • Codex reviewer: approved (PR-introduced findings fixed across 2 commits; transitive is_owner_of finding dismissed — agentkeys architecture is master → agent only, no grandchildren)
  • docs/manual-test-issue-15.md walks both fixes end-to-end

Issue

Closes #15 (parts 1+2). Part 3 (scope-edit CLI) remains open on this issue until the fix-15b PR.

🤖 Generated with Claude Code


Codex review (2026-04-14): ✅ Approved across 3 passes. Pass 1 P2s (double-read on --env override + denied /credential/list not audited) fixed. Pass 2 P2s (pre-flight ordering + empty KEY/SERVICE) fixed. Pass 3 P2 (transitive is_owner_of) dismissed — agentkeys session model is strictly master → agent (one hop), no grandchildren exist; finding chased a non-issue. Branch rebased onto main (post-#18). Final commits: 6c95942, afe66df, 647c090.

@hanwencheng
Copy link
Copy Markdown
Member Author

Codex review (via gstack /codex skill, GPT-5.4 codex-high reasoning against origin/main)

Verdict

The new --env path double-reads already injected credentials, which overstates audit activity and backend usage, and the newly introduced credential-list endpoint bypasses denied-access auditing for master-session run calls. Those are correctness regressions in the main behavior added by this patch.

Full review comments:

  • [P2] Reuse fetched credentials instead of reading override services twice — crates/agentkeys-cli/src/lib.rs:209-220
    When a --env KEY=service override names a service that is already auto-injected, this loop issues a second read_credential for the same secret instead of reusing the value fetched above. In a scoped session, agentkeys run ... --env GITHUB_TOKEN=github will therefore record two backend reads/audit events for one invocation, and any per-read accounting or rate limiting is doubled as well.

  • [P2] Audit denied credential-list requests now used by run — crates/agentkeys-mock-server/src/handlers/credential.rs:203-207
    cmd_run now calls /credential/list for master sessions, but this handler returns 403 without inserting an audit row. As a result, agentkeys run <other-agent> -- ... no longer leaves the DENIED trail that read_credential and agentkeys usage rely on, so cross-agent probing through the new path becomes invisible in the audit log.

— codex review --base main + -c 'model_reasoning_effort="high"'.

hanwencheng added a commit that referenced this pull request Apr 14, 2026
…ption

Codex v2 review flagged clap layout bug: optional Option<String> leading
positional causes runtime panic in debug ("Found non-required positional
argument with a lower index than a required positional argument") and
parse ambiguity in release. `agentkeys store openrouter sk-xxx` is
consumed as `agent=openrouter, service=sk-xxx, key=<missing>`.

Clap offers no clean way to make a leading Option + required trailing
positionals disambiguate purely on arg count. Options considered:
- `conflicts_with` / `last = true` — doesn't help.
- Subcommand split — doubles surface, confuses users.
- Dynamic-dispatch ArgMatches — sacrifices derive macros.

Chose: keep --agent as a long flag. Preserves:
- `agentkeys store openrouter sk-xxx` (session wallet)
- `agentkeys store --agent my-bot openrouter sk-xxx` (resolve alias)

Positional form `agentkeys store 0xABC openrouter sk-xxx` is NOT preserved
from pre-#19 main — documented clearly in the long_about. Scripts can
migrate with a `store 0x -> store --agent 0x` s/// pattern.

URL-encoding fix for P2 retained.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@hanwencheng
Copy link
Copy Markdown
Member Author

Codex review reply — PR #19

Codex flagged 2 × P2 findings:

  1. --env override double-reads a credential that was auto-injected in the scoped-session path. Real inefficiency (2× audit events + 2× rate-limit decrement for one invocation). Fix: populate env_vars from the --env loop using the already-fetched credentials map before falling through to a fresh read_credential. Tracked as follow-up.
  2. /credential/list doesn't audit-log denied requests (403). Real — the existing audit contract logs DENIED on read_credential, so cross-agent probing via run is currently invisible. Fix: add INSERT INTO audit_log ... 'list', 'DENIED' ... before the 403 return in list_credentials. Tracked as follow-up.

Both are correctness concerns (audit trail completeness + dup read count), not blockers. Flagging for reviewer — will land in a follow-up PR before this branch is tagged as v0 behavior.

hanwencheng added a commit that referenced this pull request Apr 14, 2026
… wallet

Codex P2 finding on PR #18: `agentkeys revoke <my-own-wallet>` revoked
the session on the backend but left the dead token cached locally; every
subsequent command loaded the stale revoked token and failed.

cmd_revoke's `Some(target_wallet_str)` branch now case-insensitively
compares target_wallet against session.wallet. On match: call
session_store::clear_session() and return a 'was your own session — re-pair'
message (matching the no-arg self-revoke form's UX).

Tests added (19 total CLI tests, +2 from previous):
- cmd_revoke_with_own_wallet_clears_local_session — verifies the new path
  wipes the local file and emits the 'was your own session' acknowledgement.
- cmd_revoke_with_other_wallet_keeps_local_session — counterpart: revoking
  someone else's wallet must NOT touch the caller's session.

P2-2 (parallel-test HOME race) is tracked separately as issue #34 — needs
serial_test crate added cross-PR (#18, #19, #20, #27 all affected).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
hanwencheng added a commit that referenced this pull request Apr 14, 2026
P2-1 (cli): cmd_run was issuing two GET /credential/read calls when an
--env override targeted a service that auto-injection had already
fetched. Cache successful reads in a HashMap<service, ciphertext> and
reuse the cached value during --env resolution. Halves credential RTTs
for the common "auto-inject + override" flow.

P2-2 (mock): list_credentials returned 403 on cross-agent attempts
without leaving an audit trail, so probing through the new
/credential/list endpoint stayed invisible. Mirror the read_credential
contract by inserting a 'list' / 'DENIED' audit row (service_name='*')
before the 403 return. Extend integration test
list_credentials_ownership_enforced to assert the DENIED row appears.

Test: cargo test -p agentkeys-cli -p agentkeys-mock-server -- --test-threads=1
mock-server: 40 passed; cli: 18 passed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@hanwencheng
Copy link
Copy Markdown
Member Author

Codex P2 fixes pushed (1a09a69)

Both findings addressed in this branch:

P2-1 — Double-read on --env override (cli)
Cache successful /credential/read responses in a HashMap<String, String> inside cmd_run. The auto-injection loop populates the cache; the --env resolution loop checks the cache first before issuing another GET. Halves credential round-trips for the common "auto-inject + override" flow.

P2-2 — Denied /credential/list not audited (mock-server)
list_credentials now mirrors the read_credential audit contract: insert a 'list' / 'DENIED' row (service_name=*) before returning 403 on ownership failure, so cross-agent probing through the new endpoint stays visible.

Test extension: list_credentials_ownership_enforced now asserts the DENIED row appears via /audit/query?agent=....

cargo test -p agentkeys-cli -p agentkeys-mock-server -- --test-threads=1
mock-server: 40 passed; cli: 18 passed

Re-running codex review --base main to confirm no remaining P1/P2.

hanwencheng added a commit that referenced this pull request Apr 14, 2026
…d I/O

Two correctness issues from the second codex pass on PR #19:

1. Pre-flight --env validation ran AFTER the master-session
   list_credentials() call, so a malformed `--env NO_EQUALS` could still
   produce a backend round-trip and a list/DENIED audit row before the
   parse error fired. Move the validation loop above the
   list_credentials branch so the pre-flight guarantee in the comment
   actually holds for the master-session path.

2. The validation only checked for the presence of '=', so `--env =foo`
   (empty KEY) and `--env BAR=` (empty SERVICE) were accepted and then
   blew up later as opaque runtime errors (empty env-var name passed to
   process spawn / empty service name passed to read_credential).
   Reject both up front with a clear "KEY must not be empty" /
   "SERVICE must not be empty" message.

Tests: cmd_run_env_flag_empty_key_rejected,
cmd_run_env_flag_empty_service_rejected.

Test: cargo test -p agentkeys-cli -- --test-threads=1 → 20 passed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@hanwencheng
Copy link
Copy Markdown
Member Author

Codex P2 v2 fixes pushed (d17b90c)

Second codex pass surfaced two more legitimate issues on the new --env plumbing:

Pre-flight ordering: validation ran after list_credentials() on the master-session branch, so --env NO_EQUALS could still trigger a backend round-trip and a list/DENIED audit row before the parse error fired. Moved the validation loop above the list_credentials branch so the pre-flight guarantee in the comment actually holds.

Empty KEY / SERVICE: --env =github and --env MY_KEY= were both accepted and then blew up later (empty env-var name to process spawn / empty service name to read_credential). Reject both up front with "KEY must not be empty" / "SERVICE must not be empty".

Tests added:

  • cmd_run_env_flag_empty_key_rejected
  • cmd_run_env_flag_empty_service_rejected
cargo test -p agentkeys-cli -- --test-threads=1
20 passed; 0 failed

Re-running codex review --base main to confirm.

@hanwencheng
Copy link
Copy Markdown
Member Author

Codex P2 v3 — out of scope, tracked in #35

Third pass surfaced one P2: is_owner_of in agentkeys-mock-server/src/auth.rs:51-70 only walks one parent_token hop, so master sessions can't list/read/store/teardown credentials of grandchildren. Codex is correct.

But this predates PR #19is_owner_of was created in commit 1a3271e (stage 1) and applies to all four credential handlers (read, store, list, teardown), not just the new /credential/list endpoint introduced here. PR #19's contribution was to route a new caller (cmd_run for master sessions) through the existing helper, which exposed the limitation more visibly.

The proper fix touches the auth model for four handlers and needs its own PR + integration tests for nested session trees. Tracked in #35.

Marking this PR's codex review as ✅ — both PR-introduced P2s (double-read on --env override, missing DENIED audit row, pre-flight ordering, empty KEY/SERVICE) are now fixed; the remaining transitive-ownership P2 is a pre-existing limitation deferred to #35.

hanwencheng and others added 3 commits April 14, 2026 21:11
…v` override

Closes #15 (parts 1+2). Part 3 (scope-edit CLI) tracked as follow-up.

On main, `cmd_run` pulled service names only from `session.scope.services`,
so master sessions (scope: None) injected nothing. This change adds a new
`CredentialBackend::list_credentials` trait method with a backing HTTP
endpoint (GET /credential/list) and uses it as the fallback when scope is
None. Also adds `--env KEY=service` as an escape hatch for services whose
canonical env var doesn't match the `SERVICE_API_KEY` convention.

Test coverage: 7 new tests (3 backend, 4 CLI); 64 total across the touched
crates, all green.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
P2-1 (cli): cmd_run was issuing two GET /credential/read calls when an
--env override targeted a service that auto-injection had already
fetched. Cache successful reads in a HashMap<service, ciphertext> and
reuse the cached value during --env resolution. Halves credential RTTs
for the common "auto-inject + override" flow.

P2-2 (mock): list_credentials returned 403 on cross-agent attempts
without leaving an audit trail, so probing through the new
/credential/list endpoint stayed invisible. Mirror the read_credential
contract by inserting a 'list' / 'DENIED' audit row (service_name='*')
before the 403 return. Extend integration test
list_credentials_ownership_enforced to assert the DENIED row appears.

Test: cargo test -p agentkeys-cli -p agentkeys-mock-server -- --test-threads=1
mock-server: 40 passed; cli: 18 passed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…d I/O

Two correctness issues from the second codex pass on PR #19:

1. Pre-flight --env validation ran AFTER the master-session
   list_credentials() call, so a malformed `--env NO_EQUALS` could still
   produce a backend round-trip and a list/DENIED audit row before the
   parse error fired. Move the validation loop above the
   list_credentials branch so the pre-flight guarantee in the comment
   actually holds for the master-session path.

2. The validation only checked for the presence of '=', so `--env =foo`
   (empty KEY) and `--env BAR=` (empty SERVICE) were accepted and then
   blew up later as opaque runtime errors (empty env-var name passed to
   process spawn / empty service name passed to read_credential).
   Reject both up front with a clear "KEY must not be empty" /
   "SERVICE must not be empty" message.

Tests: cmd_run_env_flag_empty_key_rejected,
cmd_run_env_flag_empty_service_rejected.

Test: cargo test -p agentkeys-cli -- --test-threads=1 → 20 passed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@hanwencheng
Copy link
Copy Markdown
Member Author

Rebased onto main (post-#18)

Force-pushed the branch rebased onto current main (b9f7c38, includes PR #18 merge). Conflicts resolved:

Post-rebase commits:

Tests post-rebase:

cargo test -p agentkeys-cli -p agentkeys-mock-server -- --test-threads=1
cli: 25 passed (up from 20 — picked up 5 new tests from #18's revoke changes)
mock-server: 46 passed

Codex pass-3 finding dismissed

Closed issue #35. After architectural review the agentkeys session model is strictly master → agent (one hop) — create_child_session is only ever called with a master session as parent in all test + production paths, and there is no recursive pairing API. Codex's transitive-ownership concern speculated about grandchild agents that don't exist in this system. is_owner_of's single-hop check is sufficient for the current architecture.

@hanwencheng hanwencheng merged commit 17847d1 into main Apr 14, 2026
hanwencheng added a commit that referenced this pull request Apr 14, 2026
Per human design call on PR #20: ship the --agent flag form (option 1)
and migrate every breaking script reference in our own docs.

Files updated:
- wiki/credential-usage.md — synopsis, examples, lifecycle, env-var
  mapping section all show the new form. Added a "breaking change"
  callout at the top with the sed migration recipe. Removed the stale
  "issue #15 limitation" blockquote (issue #15 is now fixed in PR #19).
- wiki/key-security.md — the read escape-hatch + run preferred-usage
  examples updated.
- wiki/data-classification.md — credential store flow diagram updated.
- crates/agentkeys-cli/src/main.rs — top-level `long_about` (shown by
  `agentkeys --help`) now uses the --agent flag form across all examples
  and explicitly documents the omit-to-use-session-wallet behavior.
- docs/contradictions.md §4.3 — corrected the resolution narrative
  (positional Option panics clap; --agent flag is the chosen disambiguation),
  documented the breaking change + migration sed.

No code change in this commit; the actual --agent flag landed in
efa1707 already.

Test: cargo test -p agentkeys-cli -- --test-threads=1 → 19 passed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
hanwencheng added a commit that referenced this pull request Apr 14, 2026
Combines the full rebase of fix/issue-16 onto main (post-#18 + #19 merges),
with all conflicts resolved in a single clean commit.

- `CredentialBackend::list_credentials` + `resolve_identity` trait methods
  live side-by-side; both `MockHttpClient` and `InProcessBackend`
  implement both.
- `cmd_run` signature is `(ctx, agent: Option<&str>, env_overrides: &[String], cmd: &[String])`:
  - `agent` from #16 (defaults to session wallet when None)
  - `env_overrides` from #19 (HashMap cache + pre-flight validation)
- `cmd_store` / `cmd_read` take `agent: Option<&str>`.
- `cmd_revoke` takes `agent: Option<&str>` (signature kept from #18).
- `main.rs` dispatches `Commands::{Store,Read,Run,Revoke} { agent, ... }`
  via `.as_deref()` since each carries `agent: Option<String>`.
- clap derive `Run` has both `--agent` flag AND `--env KEY=SERVICE` flag.
- Top-level `long_about` and all sub-command `long_about` strings show the
  new `--agent` flag form.
- `wiki/credential-usage.md` / `wiki/key-security.md` /
  `wiki/data-classification.md` migrated to the new form with a
  "breaking change" callout + sed migration recipe.
- `docs/contradictions.md` §4.3 resolution narrative corrected.
- Integration + CLI tests: 30 cli + 6 core + 46 mock-server passing.

Resolves issue #16 (wallet-optional + identity aliases) after the 3-pass
codex stalemate landed on the `--agent` flag design (human call).

Test: cargo test -p agentkeys-cli -p agentkeys-core -p agentkeys-mock-server -- --test-threads=1
hanwencheng added a commit that referenced this pull request Apr 14, 2026
…r handlers

Rebase onto main (post-#10 30-day TTL + #17 revoke + #19 credential/list)
with ENS handling and wallet-404 fixes baked in per PR #21 human design call.

## Refactor (original #13 scope)
- `handlers/identity.rs::resolve_identity_typed(db, identity_type, identity_value)`
  — single typed resolver used by `approve_auth_request` Recover branch and
  the `/identity/resolve` endpoint.
- Typed `identity_type` + `identity_value` columns on `auth_requests`.
  POST /auth-request/open now REQUIRES them for Recover and REJECTS them
  for non-Recover (400 either way).
- Modular minting: `mint_pair_session`, `mint_recover_session`,
  `mint_scope_change_session` each return a `MintOutput`. `approve_auth_request`
  is pure dispatch — no inlined per-request-type logic.

## Rebase + codex P2 fixes (new in this push)
- mint_pair_session and mint_recover_session updated to the 30-day TTL
  (2_592_000 sec) policy from #10, not the old 1-hour default.
- `resolve_identity_typed` "ens" arm added — ENS identities now resolve
  via the same identity_links lookup as alias/email (codex P2 round 1).
- `mock_client.rs` + `test_client.rs` open_auth_request Recover branches
  now serialize AgentIdentity::Ens as identity_type="ens" (was silently
  "alias").
- `resolve_identity_typed` "wallet" arm now confirms the wallet exists in
  `accounts` before returning it; unknown wallets return 404 instead of
  flowing through to a 500 on the sessions FK (codex P2 round 2).

## Tests
- 3 new regression tests:
  - `resolve_identity_ens_returns_wallet`
  - `resolve_identity_wallet_unknown_returns_not_found`
  - (+ `resolve_identity_wallet_passthrough` updated to use a real session
    wallet so the new existence check passes)
- 8 original #13 tests preserved
- cli 25 / core 6 / mock-server 56, all passing

Closes #13.
hanwencheng added a commit that referenced this pull request Apr 14, 2026
Rebase of fix/issue-12 onto current main (post-PR #18 #19 #21 #23 merges)
surfaced two concrete integration issues, fixed in this patch:

**1. Expose session_store helpers for test reuse**
- Make `agentkeys_core::session_store::fallback_path(session_id)` public
  so CLI tests can assert on the on-disk path layout.
- Re-export `agentkeys_core::session_store` at `agentkeys_cli::session_store`
  so existing test imports (`use agentkeys_cli::session_store;`) keep
  working after the old cli-local `session_store.rs` was deleted upstream.

**2. cmd_revoke clear_session calls (post-PR #18 self-revoke merge)**
PR #18 added `session_store::clear_session()` calls on self-revoke paths.
The new signature takes `session_id`, not 0 args. Pass `&ctx.session_id`
so the revoke wipes the correct namespace for any future multi-session
CLI caller.

**3. Integration tests: seed identity_links for Recover**
PR #21 tightened `resolve_identity_typed` to require the identity to
exist in `identity_links` before resolution. Two pair_tests
(`recover_full_loop`, `recover_credentials_intact`) opened Recover
requests with aliases that were never linked, so they now 404.
Added `InProcessBackend::link_identity_for_tests(identity_type,
identity_value, wallet)` that inserts directly into the mock DB's
`identity_links` table. The two failing tests now seed the alias link
before the Recover flow.

Test: cargo test -p agentkeys-core -p agentkeys-daemon -p agentkeys-cli -p agentkeys-mock-server -- --test-threads=1
core: 21; daemon: 13 + 15; cli: 30; mock-server: 56 — all passing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
hanwencheng added a commit that referenced this pull request Apr 15, 2026
Post-rebase review (claude code-reviewer) flagged 3 P1 issues that map
directly to patterns in `.github/REVIEW_GUIDELINES.md`. All fixed in
this commit plus one P2 nit that was cheap.

**P1 — URL encoding via reqwest `.query()` on scope + list_credentials**
`get_scope` and `list_credentials` in `mock_client.rs` built queries with
raw `format!()` interpolation. Wallet strings with reserved characters
(`&`, `#`, `%`, `+`, spaces) would break the request or smuggle extra
params server-side. Switched both to `.get(self.url("/path")).query(&[...])`
so reqwest percent-encodes per RFC 3986. Same pattern as the fix on PR
#20 `aa0cc95` / PR #22 `ffdc908` / PR #29 `7aa9e70`.

**P1 — Case-insensitive self-target check in `update_scope`**
The self-target reject `session.wallet_address == target_wallet` was
string-equal, but the backend stores wallets in lowercase while EIP-55
checksummed input preserves case. A master passing its own wallet in
mixed-case form would bypass the self-target guard and silently
restrict its own scope (the exact failure the guard was designed to
prevent). Switched to `eq_ignore_ascii_case` matching the pattern
established in PR #18's self-revoke detection.

**P1 — Missing DENIED audit rows on scope endpoints**
`update_scope` and `get_session_scope` returned 403 on ownership
failure without inserting an audit row, breaking the
cross-agent-probing-visibility invariant established for
`list_credentials` / `read_credential` in PR #19. Added `'scope_update'
'DENIED'` and `'scope_read' 'DENIED'` inserts before the 403 return
in both handlers.

**P2 — `--add foo --remove foo` would no-op silently**
`cmd_scope` accepted combinations like `--add X --remove X`. The add
loop would push, then `retain(!remove.contains)` would cancel, and the
backend PUT would still fire with the original scope + misleading
"Scope updated" output. Added an up-front `add ∩ remove` check that
lists overlapping services and rejects.

**P2 — Narrow UPDATE to most-recent active session**
Write-side `update_scope` UPDATEd ALL active sessions for the target
wallet; read-side `get_session_scope` always returned the scope from
the most-recent session via `ORDER BY created_at DESC LIMIT 1`.
Read/write contract mismatched on wallets with multiple active
sessions (paired + recovered). Narrowed the UPDATE to use the same
ORDER/LIMIT subquery.

Test: cargo test -p agentkeys-core -p agentkeys-cli -p agentkeys-mock-server -- --test-threads=1
core: 22 passed; cli: 35 passed; mock-server: 56 passed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
hanwencheng added a commit that referenced this pull request Apr 15, 2026
* fix(cli): #15 part 3 (fix-15b) — agentkeys scope command

Closes part 3 of #15.

Adds `agentkeys scope <AGENT>` subcommand with `--add`/`--remove`/`--set`/`--list`
for editing a child agent's scope. Under the hood: new trait methods
`CredentialBackend::get_scope` + `update_scope` backed by a new mock-server
`PUT /session/scope` endpoint (owner-checked, updates all active sessions
for the target wallet).

Tests: 5 new CLI integration tests (list/add/remove/set/conflict). 62 total
tests across cli/core/mock-server, all green under --test-threads=1.

Minor: CommandContext::load_session made pub so integration tests can
access the current session token.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(cli/mock-server): #15b v2 — address codex P1+P2

Codex P1: update_scope handler allowed a master session to target its
own wallet, silently writing a restrictive scope_json onto the master's
session row. Subsequent credential/read calls would start failing for
services outside the accidental scope. Now rejects self-targeting with
a clear bad_request.

Codex P2 (URL encoding): cmd_scope's identity resolution built the
/identity/resolve query string via raw interpolation, breaking
aliases/emails with reserved characters ('+', '&', '=', '%', spaces).
Switched to reqwest's .query() builder which percent-encodes per RFC.

Codex P3 (--list exclusivity): --list now rejects combination with
--add/--remove/--set up front instead of silently dropping the mutation.

Deferred (tracked as follow-up): CBOR vs JSON parsing of ScopeChange
request_details in mint_scope_change_session — the scope CLI uses the
direct /session/scope endpoint, not the auth-request flow, so this
finding doesn't affect the CLI path. Will be addressed when
AuthRequest::ScopeChange approval is wired end-to-end.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(cli/mock-server): #29 v3 — claude P1+P2 review fixes after rebase

Post-rebase review (claude code-reviewer) flagged 3 P1 issues that map
directly to patterns in `.github/REVIEW_GUIDELINES.md`. All fixed in
this commit plus one P2 nit that was cheap.

**P1 — URL encoding via reqwest `.query()` on scope + list_credentials**
`get_scope` and `list_credentials` in `mock_client.rs` built queries with
raw `format!()` interpolation. Wallet strings with reserved characters
(`&`, `#`, `%`, `+`, spaces) would break the request or smuggle extra
params server-side. Switched both to `.get(self.url("/path")).query(&[...])`
so reqwest percent-encodes per RFC 3986. Same pattern as the fix on PR
#20 `aa0cc95` / PR #22 `ffdc908` / PR #29 `7aa9e70`.

**P1 — Case-insensitive self-target check in `update_scope`**
The self-target reject `session.wallet_address == target_wallet` was
string-equal, but the backend stores wallets in lowercase while EIP-55
checksummed input preserves case. A master passing its own wallet in
mixed-case form would bypass the self-target guard and silently
restrict its own scope (the exact failure the guard was designed to
prevent). Switched to `eq_ignore_ascii_case` matching the pattern
established in PR #18's self-revoke detection.

**P1 — Missing DENIED audit rows on scope endpoints**
`update_scope` and `get_session_scope` returned 403 on ownership
failure without inserting an audit row, breaking the
cross-agent-probing-visibility invariant established for
`list_credentials` / `read_credential` in PR #19. Added `'scope_update'
'DENIED'` and `'scope_read' 'DENIED'` inserts before the 403 return
in both handlers.

**P2 — `--add foo --remove foo` would no-op silently**
`cmd_scope` accepted combinations like `--add X --remove X`. The add
loop would push, then `retain(!remove.contains)` would cancel, and the
backend PUT would still fire with the original scope + misleading
"Scope updated" output. Added an up-front `add ∩ remove` check that
lists overlapping services and rejects.

**P2 — Narrow UPDATE to most-recent active session**
Write-side `update_scope` UPDATEd ALL active sessions for the target
wallet; read-side `get_session_scope` always returned the scope from
the most-recent session via `ORDER BY created_at DESC LIMIT 1`.
Read/write contract mismatched on wallets with multiple active
sessions (paired + recovered). Narrowed the UPDATE to use the same
ORDER/LIMIT subquery.

Test: cargo test -p agentkeys-core -p agentkeys-cli -p agentkeys-mock-server -- --test-threads=1
core: 22 passed; cli: 35 passed; mock-server: 56 passed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(cli/mock-server): #29 v4 — claude re-review nits (dead sort, pct_encode consistency, test coverage)

Follow-up on the claude-code-action re-review posted against commit
57dc9fb. All 3 substantive findings addressed; the 4th (malformed `/`
doc-comments) was a false positive from pre-rebase state — grep for
`^\s*/ ` returns nothing in the current tree.

**Low — Redundant `sort_by` after `update_scope`**
`lib.rs:769` was re-sorting `new_scope.services` after the backend
call returned, but both the `--set` branch (line 749) and the
`--add`/`--remove` branch (line 760) sort before the `update_scope`
call. Dead op; removed. Added a comment so a future reader doesn't
re-add it.

**Low — `InProcessBackend::get_scope` raw URL concat**
`test_client.rs:725` built the query string via `format!()` while the
`resolve_identity` method right above uses `pct_encode`. Wallet
strings are hex so this was safe in practice, but the inconsistency
violates the URL-encoding invariant documented in
`.github/REVIEW_GUIDELINES.md` pattern #3. Swapped to `pct_encode`.

**Low — Missing test for `--list + --add` conflict**
Added `cmd_scope_list_and_add_conflict_errors`. Also added
`cmd_scope_add_remove_overlap_errors` (covering the P2 overlap guard
from v3) that wasn't yet under test.

Test: cargo test -p agentkeys-core -p agentkeys-cli -p agentkeys-mock-server -- --test-threads=1
core: 22 passed; cli: 37 passed (+2 new); mock-server: 56 passed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CLI run command broken for master sessions + missing scope edit and --env flag

1 participant